2005年10月28日
川俣晶の縁側ソフトウェア技術雑記 total 18149 count

これは凶悪だっっっ! JavaScriptの"for~in"構文の罠

Written By: 川俣 晶連絡先

 JavaScriptには、コレクションを列挙する"for~in"構文があります。

 これはC#のforeachや、Visual BasicのFor Eachに相当する素敵でラクチンな機能か……と思いきや。検索してみると、使用している事例を全く見かけません。

 その理由(?)らしきものを調べたので、まとめておきます。

まずはC#でforeachを試す §

 C#やVisual Basicのプログラマにとって、直感的で良く分かるソースから。

using System;

using System.Xml;

class Class1

{

    [STAThread]

    static void Main(string[] args)

    {

        string srcDoc = "<html><p>line1</p><p>line2</p><p>line3</p></html>";

        XmlDocument doc = new XmlDocument();

        doc.LoadXml( srcDoc );

        XmlNodeList pCollection = doc.GetElementsByTagName("p");

        foreach( XmlNode node in pCollection )

        {

            Console.WriteLine("[{0}] ({1})",node.ToString(),node.InnerText);

        }

    }

}

 実行結果は以下の通り。

[System.Xml.XmlElement] (line1)

[System.Xml.XmlElement] (line2)

[System.Xml.XmlElement] (line3)

 コレクションの個々のアイテムが、ループ変数nodeに入っています。

Internet Explorer 6.0で試す §

 HTML DOMからp要素を抜き出して列挙するコードを書いてみました。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>test</title>

<script><!--

function onLoad()

{

    var result = "";

    var pCollection = document.getElementsByTagName('p');

    for( var index in pCollection )

    {

        result += "<p>[" + index + "]=[" + pCollection[index] + "]</p>";

    }

    document.getElementById('result').innerHTML = result;

}

// --></script>

</head>

<body onload="onLoad()">

<p>line1</p>

<p>line2</p>

<p>line3</p>

<div  id="result">

</div>

</body>

</html>

 実行結果は以下の通り。なお、最初の3行は列挙する対象。残りが、列挙した結果です。

line1

line2

line3

[length]=[3]

[0]=[[object]]

[1]=[[object]]

[2]=[[object]]

 というわけで、C#やVisual Basicとは2つの決定的な相違が見えてきました。

 第1に、ループ変数に入ってくる値は、コレクションの値ではなく、キーであること。つまり、コレクションの値を取り出すには、キーを使ってあらためて取り出す操作を記述しなければなりません。(つまり、indexではなくpCollection[index]と書かねばならないということ)。

 第2に、オブジェクトが持っている(おそらく)全ての情報が列挙されていること。つまり、コレクションの長さを示すlengthの値も列挙された結果、3個のオブジェクトを含むコレクションを列挙させたにもかかわらず、ループは4回繰り返されます。

Firefox 1.0.7で試す §

 同じコードをFirefox 1.0.7で実行すると以下の通り。

line1

line2

line3

[0]=[[object HTMLParagraphElement]]

[1]=[[object HTMLParagraphElement]]

[2]=[[object HTMLParagraphElement]]

[length]=[3]

[item]=[ function item() { [native code] } ]

[namedItem]=[ function namedItem() { [native code] } ]

 なんと、列挙される回数が2回増えています。

Opera 8.5で試す §

 Opera 8.5で試してみると、更にびっくり。

line1

line2

line3

[0]=[[object HTMLParagraphElement]]

[1]=[[object HTMLParagraphElement]]

[2]=[[object HTMLParagraphElement]]

[length]=[3]

[item]=[ function item() { [native code] }]

[namedItem]=[ function namedItem() { [native code] }]

[tags]=[ function tags() { [native code] }]

 なんとなんと、列挙される回数がFirefoxより1回増えています。つまり、IEと比較すると3回増えています。

結論 §

 JavaScriptの"for~in"構文は、C#やVisual Basicのforeach(For Each)構文とはムードが似ているだけで機能性が全く違います。つまり、同じように使えることを期待すべきではないのでしょう。

 また、Webブラウザ間の非互換性が露呈しやすい機能であることも確認できました。様々なオブジェクトは、個々の環境に固有の情報が追加される可能性があり得ますが、"for~in"構文の利用はその相違に対して敏感に反応してしまいます。

 というわけで、"for~in"構文は使わない方が安全かつ確実、ということのようです。利用例がほとんど見付からないのも当然ですね。

メモ §

 ECMA-262に書かれている"DontEnum 属性"は、この件に関係があるのだろうか?